package com.fiona.common.extension;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fiona.annotation.FionaSPI;
import com.fiona.core.util.Holder;

public final class ExtensionLoader<T> {
	private static Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
	private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
	private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
	private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();

	private final Class<?> type;
	private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
	private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

	private ExtensionLoader(Class<?> type) {
		this.type = type;
	}

	public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {
		if (type == null) {
			throw new IllegalArgumentException("Extension type should not be null.");
		}
		if (!type.isInterface()) {
			throw new IllegalArgumentException("Extension type must be an interface.");
		}
		if (type.getAnnotation(FionaSPI.class) == null) {
			throw new IllegalArgumentException("Extension type must be annotated by @FionaSPI");
		}
		// firstly get from cache, if not hit, create one
		ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
		if (extensionLoader == null) {
			EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
			extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
		}
		return extensionLoader;
	}

	public T getExtension(String name) {
		if (name == null || name.isEmpty()) {
			throw new IllegalArgumentException("Extension name should not be null or empty.");
		}
		// firstly get from cache, if not hit, create one
		Holder<Object> holder = cachedInstances.get(name);
		if (holder == null) {
			cachedInstances.putIfAbsent(name, new Holder<>());
			holder = cachedInstances.get(name);
		}
		// create a singleton if no instance exists
		Object instance = holder.get();
		if (instance == null) {
			synchronized (holder) {
				instance = holder.get();
				if (instance == null) {
					instance = createExtension(name);
					holder.set(instance);
				}
			}
		}
		return (T) instance;
	}

	public List<T> getExtensionList() {
		Map<String, Class<?>> clazzMap = getExtensionClasses();
		List<T> exts = new ArrayList<T>(clazzMap.size());
		for (Entry<String, Class<?>> entry : clazzMap.entrySet()) {
			exts.add(getExtension(entry.getKey()));
		}
		exts.sort((o1, o2) -> {
			Extension p1 = o1.getClass().getAnnotation(Extension.class);
			Extension p2 = o2.getClass().getAnnotation(Extension.class);
			if (p1 == null) {
				return 1;
			} else if (p2 == null) {
				return -1;
			} else {
				return p1.order() - p2.order();
			}
		});

		return exts;
	}

	private T createExtension(String name) {
		// load all extension classes of type T from file and get specific one by name
		Class<?> clazz = getExtensionClasses().get(name);
		if (clazz == null) {
			throw new RuntimeException("No such extension of name " + name);
		}
		T instance = (T) EXTENSION_INSTANCES.get(clazz);
		if (instance == null) {
			try {
				EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
				instance = (T) EXTENSION_INSTANCES.get(clazz);
			} catch (Exception e) {
				logger.error(e.getMessage());
			}
		}
		return instance;
	}

	private Map<String, Class<?>> getExtensionClasses() {
		// get the loaded extension class from the cache
		Map<String, Class<?>> classes = cachedClasses.get();
		// double check
		if (classes == null) {
			synchronized (cachedClasses) {
				classes = cachedClasses.get();
				if (classes == null) {
					classes = new HashMap<>();
					// load all extensions from our extensions directory
					loadDirectory(classes);
					cachedClasses.set(classes);
				}
			}
		}
		return classes;
	}

	private void loadDirectory(Map<String, Class<?>> extensionClasses) {
		String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
		try {
			Enumeration<URL> urls;
			ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
			urls = classLoader.getResources(fileName);
			if (urls != null) {
				while (urls.hasMoreElements()) {
					URL resourceUrl = urls.nextElement();
					loadResource(extensionClasses, classLoader, resourceUrl);
				}
			}
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}

	private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
		try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
			String line;
			// read every line
			while ((line = reader.readLine()) != null) {
				// get index of comment
				final int ci = line.indexOf('#');
				if (ci >= 0) {
					// string after # is comment so we ignore it
					line = line.substring(0, ci);
				}
				line = line.trim();
				if (line.length() > 0) {
					try {
						final int ei = line.indexOf('=');
						String name = line.substring(0, ei).trim();
						String clazzName = line.substring(ei + 1).trim();
						// our SPI use key-value pair so both of them must not be empty
						if (name.length() > 0 && clazzName.length() > 0) {
							Class<?> clazz = classLoader.loadClass(clazzName);
							extensionClasses.put(name, clazz);
						}
					} catch (ClassNotFoundException e) {
						logger.error(e.getMessage());
					}
				}

			}
		} catch (IOException e) {
			logger.error(e.getMessage());
		}
	}
}
